<?php
/**
 * 数据库备份与还原
 */
define ('BACKUP_PATH',DATA_DIR.'Backup/');

class DatabaseApp extends CommonApp{
  
    
    
	/**
     * 数据库备份/还原列表
     * @param  String $type import-还原，export-备份
     */
    public function indexAct(){
    	$type = in_array(My::arr2var($_GET,'type'),array('import','export','schema'))?My::arr2var($_GET,'type'):'export';//默认:备份
        switch ($type) {
            /* 数据还原 */
            case 'import':
                //列出备份文件列表
                $path = BACKUP_PATH;
                if(!is_dir($path))
                    mk_dir($path) || $this->ajaxReturn(404,'备份目录不存在！');
                $path = realpath($path);
                $flag = FilesystemIterator::KEY_AS_FILENAME;
                $glob = new FilesystemIterator($path,  $flag);
                $list = array();
                foreach ($glob as $name => $file) {
                    if(preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql(?:\.gz)?$/', $name)){
                        $name = sscanf($name, '%4s%2s%2s-%2s%2s%2s-%d');

                        $date = "{$name[0]}-{$name[1]}-{$name[2]}";
                        $time = "{$name[3]}:{$name[4]}:{$name[5]}";
                        $part = $name[6];

                        if(isset($list["{$date} {$time}"])){
                            $info = $list["{$date} {$time}"];
                            $info['part'] = max($info['part'], $part);
                            $info['size'] = $info['size'] + $file->getSize();
                        } else {
                            $info['part'] = $part;
                            $info['size'] = $file->getSize();
                        }

                        $extension        = strtoupper($file->getExtension());
                        $info['compress'] = ($extension === 'SQL') ? '-' : $extension;
                        $info['time']     = strtotime("{$date} {$time}");

                        $list["{$date} {$time}"] = $info;
                    }
                }
                $baktime = My::arr2one($list,'time');
                array_multisort($baktime, SORT_DESC, $list);
                $title = '数据还原';
                break;

            /* 数据备份 */
            case 'export':
                $list  = DB('')->query('SHOW TABLE STATUS');
                $list  = array_map('array_change_key_case', $list);
                $title = '数据备份';
                break;
            /* 数据字典 */
            case 'schema':
                $list  = DB('')->query('SHOW TABLE STATUS');
                $list  = array_map('array_change_key_case', $list);
                $title = '数据字典';
                break;
            default:
                $this->ajaxReturn(300,'参数错误！');
        }

        //渲染模板
        $this->assign('meta_title', $title);
        $this->assign('list', $list);
        $this->display('Database/'.$type);
    }

    /**
     * 优化表
     * @param  String $tables 表名
     */
    public function optimizeAct(){
    	$tables = My::arr2var($_POST,'tables');
        if($tables) {
            if(is_array($tables)){
                $tables = implode('`,`', $tables);
                $list = DB('')->execute("OPTIMIZE TABLE `{$tables}`");
                if($list){
                    $this->ajaxReturn(200,"数据表优化完成！");
                } else {
                    $this->ajaxReturn(300,"数据表优化出错请重试！");
                }
            } else {
                $list = DB('')->execute("OPTIMIZE TABLE `{$tables}`");
                if($list){
                    $this->ajaxReturn(200,"数据表'{$tables}'优化完成！");
                } else {
                    $this->ajaxReturn(300,"数据表'{$tables}'优化出错请重试！");
                }
            }
        } else {
            $this->ajaxReturn(300,"请指定要优化的表！");
        }
    }

    /**
     * 修复表
     * @param  String $tables 表名
     */
    public function repairAct(){
    	$tables = My::arr2var($_POST,'tables');
        if($tables) {
            if(is_array($tables)){
                $tables = implode('`,`', $tables);
                $list = DB('')->execute("REPAIR TABLE `{$tables}`");

                if($list){
                    $this->ajaxReturn(200,"数据表修复完成！");
                } else {
                    $this->ajaxReturn(300,"数据表修复出错请重试！");
                }
            } else {
                $list = DB('')->execute("REPAIR TABLE `{$tables}`");
                if($list){
                    $this->ajaxReturn(200,"数据表'{$tables}'修复完成！");
                } else {
                    $this->ajaxReturn(300,"数据表'{$tables}'修复出错请重试！");
                }
            }
        } else {
            $this->ajaxReturn(300,"请指定要修复的表！");
        }
    }

    /**
     * 删除备份文件
     * @param  Integer $time 备份时间
     */
    public function delAct(){
    	$time = I('get.time');
        if($time){
            $name  = date('Ymd-His', $time) . '-*.sql*';
            $path  = realpath(BACKUP_PATH) . DIRECTORY_SEPARATOR . $name;
			array_map("unlink", glob($path));
            if(count(glob($path))){
                $this->ajaxReturn(200,'备份文件删除失败，请检查权限！');
            } else {
                $this->ajaxReturn(200,'备份文件删除成功！');
            }
        } else {
            $this->ajaxReturn(300,'参数错误！');
        }
    }

    /**
     * 备份数据库
     * @param  String  $tables 表名
     * @param  Integer $id     表ID
     * @param  Integer $start  起始行数
     */
    public function exportAct(){
    	$tables = My::arr2var($_POST,'tables');
        if(IS_POST && !empty($tables) && is_array($tables)){ //初始化
            //读取备份配置
            $config = array(
                'path'     => BACKUP_PATH,
                'part'     => 20971520,//数据库备份卷大小（该值用于限制压缩后的分卷最大长度。单位：B；建议设置20M）
                'compress' => 1,//数据库备份文件是否启用压缩（压缩备份文件需要PHP环境支持gzopen,gzwrite函数）
                'level'    => 9,//数据库备份文件压缩级别（数据库备份文件的压缩级别，该配置在开启压缩时生效）
            );

            //检查备份目录是否存在，不存在时创建目录
            if(!is_dir($config['path'])){
                $result = mk_dir($config['path']);
                if(!$result)
                    $this->ajaxReturn(300,'创建备份目录失败，可能是Data目录没有写权限！');
            }else{
                //检查备份目录是否可写
                is_writeable($config['path']) || $this->ajaxReturn(300,'备份目录不可写，请检查后重试！');
            }
            //检查是否有正在执行的任务
            $lock = "{$config['path']}backup.lock";
            if(is_file($lock)){
                $this->ajaxReturn(300,'检测到有一个备份任务正在执行，请稍后再试！');
            } else {
                //创建锁文件
                file_put_contents($lock, NOW_TIME);
            }         
            session('backup_config', $config);

            //生成备份文件信息
            $file = array(
                'name' => date('Ymd-His', NOW_TIME),
                'part' => 1,
            );
            session('backup_file', $file);

            //缓存要备份的表
            session('backup_tables', $tables);

            //创建备份文件
            $Database = new Database($file, $config);
            if(false !== $Database->create()){
                self::backup();//备份
                $this->ajaxReturn(200,'备份完成!');
            } else {
                $this->ajaxReturn(300,'初始化失败，备份文件创建失败！');
            }
        } else { //出错
            $this->ajaxReturn(300,'参数错误！');
        }
    }

    private function backup(){
    	set_time_limit(0);
    	$tables = session('backup_tables');
        //备份指定表
        $Database = new Database(session('backup_file'), session('backup_config'));
        //循环处理
        foreach($tables as $table){
        	$start = 0;
        	while(1){
		        $start  = $Database->backup($table, $start);
		        if(false === $start){ //出错
		            $this->ajaxReturn(300,'备份出错！');
		        } elseif (0 === $start) { //下一表
		            break;
		        } else {//当前表超过备份卷大小时,继续备份当前表
		            $start = $start[0];
		        }
	        }
        }
        //备份完成，清空缓存
        unlink(session('backup_config.path') . 'backup.lock');
        session('backup_tables', null);
        session('backup_file', null);
        session('backup_config', null);
    }

    /**
     * 还原数据库
     */
    public function importAct(){
        set_time_limit (0);
        $time = My::arr2var($_GET,'time') ?  I('get.time') : 0;
        $part = isset($_GET['part']) ?  $_GET['part'] : null;
        $start = isset($_GET['start']) ?  $_GET['start'] : null;
        if(is_numeric($time) && is_null($part) && is_null($start)){ //初始化
            //获取备份文件信息
            $name  = date('Ymd-His', $time) . '-*.sql*';
            $path  = realpath(BACKUP_PATH) . DIRECTORY_SEPARATOR . $name;
            $files = glob($path);
            $list  = array();
            foreach($files as $name){
                $basename = basename($name);
                $match    = sscanf($basename, '%4s%2s%2s-%2s%2s%2s-%d');
                $gz       = preg_match('/^\d{8,8}-\d{6,6}-\d+\.sql.gz$/', $basename);
                $list[$match[6]] = array($match[6], $name, $gz);
            }
            ksort($list);

            //检测文件正确性
            $last = end($list);
            if(count($list) === $last[0]){
                session('backup_list', $list); //缓存备份列表
                $this->ajaxReturn(200,'初始化完成！', '','', array('part' => 1, 'start' => 0));
            } else {
                $this->ajaxReturn(300,'备份文件可能已经损坏，请检查！');
            }
        } elseif(is_numeric($part) && is_numeric($start)) {
            $list  = session('backup_list');
            $db = new Database($list[$part], array(
                'path'     => realpath(BACKUP_PATH) . DIRECTORY_SEPARATOR,
                'compress' => $list[$part][2]));

            $start = $db->import($start);

            if(false === $start){
                $this->ajaxReturn(300,'还原数据出错！');
            } elseif(0 === $start) { //下一卷
                if(isset($list[++$part])){
                    $data = array('part' => $part, 'start' => 0);
                    $this->ajaxReturn(200,"正在还原...#{$part}", '','', $data);
                } else {
                    session('backup_list', null);
                    $this->ajaxReturn(200,'还原完成！');
                }
            } else {
                $data = array('part' => $part, 'start' => $start[0]);
                if($start[1]){
                    $rate = floor(100 * ($start[0] / $start[1]));
                    $this->ajaxReturn(200,"正在还原...#{$part} ({$rate}%)", '','', $data);
                } else {
                    $data['gz'] = 1;
                    $this->ajaxReturn(200,"正在还原...#{$part}", '','', $data);
                }
            }

        } else {
            $this->ajaxReturn(300,'参数错误！');
        }
    }
    
    
    
    /**
     * 数据字典详情
     */
    public function detailAct(){
        //取得所有表
        $database   = C('DB_NAME'); //数据库名
   
        //获取当前数据表名称
        $current_table['table_name'] = I('get.name');
        
        //获取当前表的详细信息
        $sql  = 'SELECT * FROM ';
        $sql .= 'INFORMATION_SCHEMA.TABLES ';
        $sql .= 'WHERE ';
        $sql .= "table_name = '{$current_table['table_name']}' AND table_schema = '{$database}'";
        $current_table_info = M()->query($sql);
        
        //获取当前表的备注
        $current_table['table_comment'] = My::arr2var($current_table_info,'0.TABLE_COMMENT');//$current_table_info[0]['TABLE_COMMENT'];
        
        //获取当前表的字段详细信息
        $sql  = 'SELECT * FROM ';
        $sql .= 'INFORMATION_SCHEMA.COLUMNS ';
        $sql .= 'WHERE ';
        $sql .= "table_name = '{$current_table['table_name']}' AND table_schema = '{$database}'";
        $current_table_columns_info = M()->query($sql);
        
        //获取当前表的字段信息
        $current_table['fields'] = $current_table_columns_info;
        
        $this->assign('list', $current_table['fields']);
        $this->assign('title', $current_table['table_name']);
        $this->display();
    }

}